tools/qemu-wasm: PebbleOS emulator in the browser (QEMU→wasm, app install, PebbleKit JS) - #1910
tools/qemu-wasm: PebbleOS emulator in the browser (QEMU→wasm, app install, PebbleKit JS)#1910ericmigi wants to merge 30 commits into
Conversation
Resurrect the PebbleOS-in-the-browser emulator on top of main's QEMU instead of the ericmigi/pebble-qemu-wasm STM32-model port it derives from. coredevices/qemu pebble-10.1 (10.1.5-pebble17) builds for emscripten out of the box, and the generic virtual machines (pebble-emery/flint/gabbro) boot the qemu_* release flash images directly, so the browser build now tracks mainline firmware rather than a frozen SDK image. The shell renders the display by polling shared-memory exports and injects buttons through an atomics bitmask, both provided by a small emscripten-only bridge in the QEMU tree (patches/0001, to be upstreamed to coredevices/qemu): a virtual-clock timer drives console updates under -display none and the page blits the rendered surface, so no SDL/proxying machinery is involved. Included: reproducible native build script (emsdk 3.1.50 + wasm32 deps), release-firmware fetcher, COOP/COEP dev server, and a node headless smoke test. Measured on 4 cores under node: serial banner at ~3 s, launcher + 'Ready for communication.' at ~55 s, 200x228 frames rendering throughout (TCI interpreter, no JIT yet). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Add an assembler that packs QEMU wasm, runtime JS, pthread worker, and the release firmware into one self-contained HTML page (gzip + base64, unpacked in-browser via DecompressionStream), with an environment probe that reports exactly which host capability is missing (COOP/COEP, SharedArrayBuffer, blob workers) instead of hanging on an incompatible host. Includes a headless chromium test driving both the boot and the diagnostics paths. To fit single-page size limits, document the Pebble-only QEMU build: --without-default-devices --with-devices-arm=pebble with a one-line devices config, which drops every non-Pebble ARM board and shrinks the wasm from 39 MB to 25 MB (13.8 MB page total). That surfaced a missing 'select I2C' in the PEBBLE Kconfig entry, now part of the refreshed browser-glue patch. Verified: the assembled page boots v4.35.0 emery firmware under chromium with pixels on canvas, and renders the correct diagnostic when served without cross-origin isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Transport-only directory (see its README): the assembled emulator site for ericmigi/pebble-qemu-wasm's GitHub Pages, so a session with push access there can deploy without redoing the wasm build. Verified under a headerless static host with coi-serviceworker providing isolation: crossOriginIsolated true, firmware boots, display renders. Remove before merging this branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Fold the measured wasm performance work into the browser-glue patch: - pebble_display framebuffer becomes plain RAM (memory_region_init_ram_ptr) instead of trapping MMIO: guest pixel writes were tens of thousands of interpreted slow-path exits per frame. The CTRL_UPDATE doorbell is the only sync point the firmware relies on, so nothing needs to trap. - inline TLB fast path in the TCI interpreter's tci_qemu_ld/st (ported from the ericmigi/pebble-qemu-wasm work): on a TLB hit with no flags, access host memory directly instead of the helper_*_mmu call chain. - skip cpu_io_recompile under emscripten (longjmp is a JS exception). - -sASYNCIFY_REMOVE for the interpreter hot path, -Doptimization=3, mimalloc. Continuous launcher scroll on pebble-emery v4.35.0 under node (4 shared cores): 2.1 -> 4.5 fps; boot to 'Ready for communication' 91 s -> 38 s; boot animation 1.3 -> 6.9 fps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
The TCI interpreter plateaus at ~4.5 fps on continuous launcher scrolling. Port the generic-machine device set onto ktock/qemu-wasm branch wasm64-tcg-b (QEMU 10.2.50 with the TCG WebAssembly JIT backend), which compiles hot translation blocks to native wasm: boot to 'Ready for communication' 91 s -> 38 s -> 15 s continuous launcher scroll 2.1 -> 4.5 -> 16.9 fps (baseline TCI -> patched TCI -> JIT, node on 4 shared cores) tools/qemu-wasm/jit/ carries the overlay (device set ported to the QEMU 10.2 APIs: hw/core header moves, CharBackend->CharFrontend, machines must declare arm_machine_interfaces to be visible in the single-binary machine registry, pebble_audio rewritten as a register-compatible drain-only stub since the audio subsystem was rewritten, pebble_set_button_state moved into pebble_gpio.c), the meson/Kconfig wiring patch, the MEMORY64=2 dependency build script, and a README with the build recipe and the API-drift map. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Under the JIT build (-sMEMORY64=2 with WASM_BIGINT) pointer-returning glue exports arrive in JS as BigInt, and the blit path's pointer arithmetic throws 'Cannot mix BigInt and other types'. Wrap the two pointer exports in Number(); a no-op for the wasm32 TCI build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
The page imported ./qemu-system-arm.mjs while every deployment lays the emscripten output down under its original .js name; browsers resolve module type from MIME, not extension, so standardize on .js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Swap the deployable bundle's emulator to the TCG-to-wasm JIT build (9 MB wasm, no separate worker file, boots to the watchface in ~15 s at ~17 fps scrolling) and carry the updated shell. Verified booting under a headerless static host with coi-serviceworker in chromium. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
The pebble-tool channel (UART1, QemuProtocol framing) previously had no transport in the browser build. Add an emscripten-only bridge inside pebble_control: pebble_wasm_serial_ctrl() exports a pair of ring buffers; the page produces host->guest bytes into the rx ring with Atomics and a 2 ms virtual-clock timer feeds them through the normal chardev receive path, while all watch-bound writes are mirrored into the tx ring for the page to drain. Works alongside a real chardev, and pebble_control is now created even when serial1 has none. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Implement the phone side of the app-install flow in JS on top of the UART1 ring bridge: - pebble-transport.js: QemuProtocol framing (0xFEED/0xBEEF), Pebble Protocol stream reassembly in both directions, and the endpoint-17 version handshake. The response is V3 with capabilities 0xA3 because the firmware replaces (not ORs) session capabilities, and losing CommSessionRunState would break launch notifications. - pbw.js: .pbw (zip) reader on the native DecompressionStream with platform-directory selection (emery -> basalt -> diorite -> aplite). - app-install.js: BlobDB INSERT of the 126-byte AppDBEntry (retrying on TRY_LATER), app_run_state RUN to trigger the watch's AppFetch request, then PutBytes INIT/PUT/COMMIT/INSTALL per object with the legacy defective CRC (verified against the firmware's test vectors). Legacy struct-version binaries read the uuid at its pre-3.0 offset. - store.js: resolves apps.repebble.com / apps.rebble.io links via the CORS-enabled appstore API and downloads the release pbw. - index.html: install panel (paste a store link) plus drag-drop of .pbw files onto the page, with transfer progress. Verified headless under node and in Chromium against the JIT build: BlobDB ack, AppFetch round-trip, CRC-clean PutBytes commits, and the watch launching the installed watchface (app fetch result 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
New JIT build with the serial bridge, the install JS modules, and the updated shell page; apply.sh now copies the module files. Document the serial bridge and install flow in the README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
File-picker alternative to drag-drop for installing local .pbw files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Apps with a pebble-js-app.js now get their JS executed when the watch launches them, wired to the watch over AppMessage: - appmessage.js: endpoint 0x30 client. PUSH carries the app uuid and a little-endian tuple dictionary; ACK/NACK are two bytes echoing the transaction id. Sends are serialized through a queue (one PUSH in flight, 10 s ACK timeout) and pushes that arrive while the app's JS is still starting are buffered and delivered after 'ready', since the watch's first outbox send races the run-state notification. - pkjs-runtime.js: sandbox runner + Pebble shim (addEventListener, sendAppMessage, tokens, getActiveWatchInfo, openURL; timeline APIs stubbed). Lifecycle follows app_run_state notifications on endpoint 52. In the browser the sandbox is a hidden same-origin iframe -- window globals like localStorage are getter-only, so the shim bindings are installed with defineProperty -- with per-app-scoped storage and an XHR-over-fetch shim that retries failed cross-origin requests through a configurable CORS proxy (?pkjs_proxy=). - config pages: openURL appends the return_to convention pointing at config-return.html, which posts the response fragment back to the page for the webviewclosed event. - pbw.js extracts pebble-js-app.js; index.html registers installed apps with the runtime and adds a settings button that triggers showConfiguration. Verified headless (node:vm sandbox) and in Chromium: BlobDB install, watch-initiated AppMessage push delivered to JS, XHR fetch, and the reply rendered by the C app on the emulated display. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Cloudflare Worker the emulator page falls back to when a JS app's API call has no CORS headers. Origin-allowlisted, GET/HEAD/POST only, credentials stripped both directions, internal hostnames refused. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Add the runtime modules and config-return page to site-dist and apply.sh; document the PKJS architecture in the README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
apply.sh now copies pkjs-proxy/ into the target checkout so the worker source lives alongside the site it serves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 18dcb6284e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const frame = document.createElement('iframe'); | ||
| frame.style.display = 'none'; | ||
| frame.setAttribute('allow', 'geolocation *'); | ||
| document.body.appendChild(frame); |
There was a problem hiding this comment.
Isolate installed app code from the page origin
When a user installs any third-party PBW containing pebble-js-app.js, this iframe has neither a sandbox attribute nor a separate origin, and the code is later executed with w.eval. Such an app can access parent.document, the emulator's full API, and all localStorage belonging to the hosting origin; replacing the iframe's localStorage property does not restrict access through parent. Run PBW code in a genuinely isolated iframe or worker and expose only the Pebble bridge through messaging.
Useful? React with 👍 / 👎.
| console.log('screenshot: ' + shot); | ||
| await browser.close(); | ||
| server.close(); | ||
| process.exit(0); |
There was a problem hiding this comment.
Return failure when the browser boot test times out
If the page never reaches the expected display state—for example because QEMU aborts, pixels remain blank, or the loop simply reaches waitS—execution falls through and exits successfully anyway. This makes the documented coi browser test report a green status for broken builds; track whether the success predicate was reached and return nonzero when it was not.
Useful? React with 👍 / 👎.
| flint: ['diorite', 'aplite', 'basalt'], | ||
| }; |
There was a problem hiding this comment.
Add the Chalk fallback for Gabbro
When the board selector passes gabbro, the missing map entry makes the fallback list just ['gabbro'], so PBWs containing the compatible chalk/pebble-app.bin are rejected as having no Gabbro binary. The firmware explicitly maps Gabbro to PlatformTypeChalk for pre-4.2 SDKs in src/fw/process_management/pebble_process_md.c:230-250, so the browser should include Chalk in Gabbro's platform preference.
Useful? React with 👍 / 👎.
| EMSDK4=/home/user/emsdk-4 | ||
| TARGET=/home/user/wasm-deps64 | ||
| SRC=/home/user/wasm64-src |
There was a problem hiding this comment.
Make the JIT dependency paths configurable
Running the documented ./build-deps64.sh command under any account other than the original /home/user environment writes outside the caller's home and commonly fails at the initial mkdir with a permission error. Later steps also copy sources from /home/user/wasm-deps, so even changing permissions does not make the build self-contained; derive these paths from $HOME or accept a work directory argument.
Useful? React with 👍 / 👎.
Point the proxy fallback at pkjs-proxy.ericmigi.workers.dev and use it for store lookups and pbw downloads too, so store installs work before the asset bucket gets a CORS policy. Accept messageKeys as the appKeys fallback for modern pbws. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Browsers block window.open on data: URLs, which is how Clay-based apps deliver their settings pages. Decode the data: URL, rewrite Clay's default pebblejs://close target to the same-origin return page, and render the page in a modal iframe; the return page now posts to parent as well as opener. Hosted config pages keep the popup + return_to path. Verified with a Clay-style page round-tripping its settings payload in Chromium. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
JS-side activity (app start, fetch failures, geolocation denials) was only visible in devtools; mirror it into the page's console panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
A silent OS-level location denial or a doubly-failed weather fetch was invisible; wrap the sandbox geolocation methods and log XHR/fetch failures and proxy fallbacks into the page console. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Call the top page's geolocation rather than the sandbox iframe's (removes the permissions-delegation variable), fall back to IP-based city-level location when the browser provider fails (common when OS location services are off), and support a ?loc=lat,lon override. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Touch: pebble_touch.c gains a browser injection path — the page writes
{seq, down, x, y} in display pixels with Atomics and an 8 ms
virtual-clock poll raises the touch IRQ on state changes, mirroring
the input-layer handler. Canvas pointer events drive it; verified in
Chromium by touch-scrolling the launcher.
Audio: the pebble_audio drain stub now mirrors every DATA sample into
a shared int16 ring with rate/volume/playing mirrors; the page drains
it into WebAudio on a 60 ms poll, scheduling buffers at a running
playhead. The context is created on the first canvas touch to satisfy
autoplay policies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
- The 32 MB SPI flash is saved to OPFS when dirty (15 s cadence, strided FNV change detection) and restored on the next boot, so installed apps and settings survive reloads; PebbleKit JS sources are persisted alongside in localStorage. Factory reset wipes both. - The page boots automatically on load (?noauto restores the button); switching boards reloads onto the chosen machine. - gabbro (getafix_dvt) is a first-class board option with v4.35.0 firmware in the site bundle. - dbgserial (UART2) gains a browser input ring behind a wasm-console device property, giving the console panel a working prompt. - Sensors & phone panel injects battery, compass, tap and time-format QemuProtocol frames over the existing serial bridge. Verified in Chromium: autoboot, install -> OPFS save -> reload -> restore (no re-fetch, JS registry intact), and the dbgserial prompt answering 'help'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
- pulse-console.js: full PULSEv2 client. Frame layer (0x55 flags, COBS, 0x55<->0x00 transparency, CRC-32 residue check), PPP control FSMs for LCP and the reliable-transport NCP, and a stop-and-wait LAPB client for the TRAIN transport. Logs (push 0x5021 port 3, the 29-byte <c16sccQH record) render as [time] <lvl> file:line: message -- the qemu build is plaintext PULSE, not hashed, so no dictionary is needed. The console input line drives the dbgserial prompt over the reliable transport (port 0x3e20). - phone-extras.js: notifications and voice. buildNotification serializes a 46-byte SerializedTimelineItemHeader plus attribute and action lists (status must be 0, payload_length exact) into a BlobDB insert on the notifs db; the default notification carries a Response action with canned replies so the Reply -> Voice path works. VoiceService accepts dictation session setups on endpoint 11000, counts the (silent) Speex frames on 10000, and on stop returns a one-sentence transcription from browser speech recognition with a typed-prompt fallback, echoing app_initiated flags and app uuid. Verified in Chromium end to end: parsed log lines, a 'version' prompt round trip, a notification banner with working action menu, and Reply -> Voice dictation displaying the browser-provided transcript. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Auto-boot disabled the dropdown before it could ever be used, making board switches impossible. The selector now stays enabled; changing it reloads the page onto the chosen machine, which is the only way to restart the emulator anyway. Verified in Chromium: switching from a running emery to gabbro boots the round 260x260 machine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
The global key handler consumed Enter and arrows even while typing in the install/console inputs, pressing Select on the watch instead of submitting the field. Ignore watch-key events when focus is in a form field, and blur buttons after clicks so the next Enter goes back to the watch rather than re-clicking the button. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
apply.sh listed JS files by name and was never taught about pulse-console.js and phone-extras.js, so deployed pages imported a 404 and the whole module script (install button included) died silently. Copy *.js wholesale, and surface module-load failures in the status line. Verified by running the full page test against an apply.sh-produced directory rather than a hand-staged one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
gmarull
left a comment
There was a problem hiding this comment.
Sources likely do not belong to this repo, this is massive, not a simple script. Maybe more patches to our qemu fork, and ship with pebbleos sdk as we do for other stuff.
Groundwork for building a Pebble app from source in the browser, using
the prebuilt wasm LLVM toolchain (clang.wasm / lld.wasm) from the
pebble-sdk-browser-compile work. That branch validated the toolchain
under Node with the stock Python SDK tools; this is the browser host
layer its README leaves open.
- wasi-run.js: runs a WASI tool against an in-memory filesystem. The
toolchain modules are wasm32-wasi-threads builds, so they import
shared linear memory rather than exporting it, and want a
thread-spawn hook they never call; both are handled here. The
vendored shim defaults its path-lookup tracing on when constructed
without options, which is far too noisy and slow for a real compile.
- codegen.js: the waf-generated files, byte-for-byte -- appinfo.auto.c,
resource_ids.auto.{c,h}, message_keys.auto.{c,h}, pebble_app.ld and
appinfo.json.
- elfpack.js: objcopy, inject_metadata and the pbw/manifest writers
ported from the SDK's Python, including the legacy defective CRC.
- jsbundle.js: esbuild stands in for the SDK's webpack step, matching
it where it matters (shared additions first, message_keys and
app_package.json virtual modules, src/common on the resolve path).
- appbuilder.js drives the pipeline; appbuild-worker.js hosts it off
the main thread and caches the toolchain download.
make-sdkpack.sh assembles what the compiler needs beyond the two tool
binaries: SDK headers and libpebble.a, newlib headers, libgcc/libc for
thumb/v7-m, clang's builtin headers, and the SDK's pkjs helper modules.
Validated under Node against a golden waf build of pebble-timer: all 11
sources compile, the link is byte-identical in structure to the waf
one, and the resulting pbw installs and runs in the emulator. In
Chromium, clang.wasm decompresses, compiles and builds an SDK source in
under a second.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Ports the SDK's waf resource generators to JS so an app's images, fonts and raw assets can be turned into an app_resources.pbpack client-side: pbpack.py for the container, bitmapgen.py for PBI, png2pblpng.py plus pebble_image_routines.py for the palettised PNGs, and font/fontgen.py for PFO v3. PNG decode and encode are done here rather than through the canvas so the output is identical under Node and in a worker; zlib comes from the streams API. FreeType is not available, so glyph rasterisation is delegated to the host through a rasterizeGlyph callback -- canvas in the browser, and in the test the golden font's own glyphs, which isolates the serialiser. Two SDK behaviours are deliberately reproduced rather than corrected, because matching waf's output is the point: bitmapgen._set_bbox() transposes its columns back into rows before computing the right edge, so empty right-hand columns are never cropped; and a font's table offsets are relative to the sfnt header, which is not always at the start of the file (the pebble-timer font carries a 260-byte prefix). Validated against a golden waf build of pebble-timer on emery: the pbpack container is byte-exact, 10 of 17 resources are byte-exact including all four fonts, and the rest -- where the SDK's palette order comes from a Python set and so cannot be reproduced -- decode to identical pixels. Building the whole app from a clean checkout now takes 2.3s under Node, and the resulting pbw runs in the emulator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Paste a repo holding an SDK app into the emulator page and it compiles client-side and installs what it built -- no SDK, no toolchain, no server. Building coredevices/pebble-timer takes about four seconds once the tools are cached. Wires the compiler into the page: a repo URL box next to the install one, a worker that survives across builds, and the codeload zip URLs a repo (or a /tree/<ref>) maps to. GitHub does not send CORS headers on codeload, so the fetch falls back to the pkjs proxy already used for app JS. The tools themselves (gzipped clang.wasm and lld.wasm, esbuild, and the emery SDK pack -- 39 MB together) ship in web/tools/ and are cached in the Cache API after the first build. apply.sh copies them, the page modules and the vendored shims into a deploy, taking them from web/ rather than site-dist/ so they are not stored twice. A build saturates every core, which starves QEMU: the watch would fall far enough behind that the first BlobDB insert timed out and the install failed outright. Give the emulator a moment before installing, and treat a silent watch as retryable there rather than fatal. Also fixes two things found by running it: the worker resolved the tools directory against its own URL one level down, and Chromium rejects a font whose sfnt header is not at the start of the file (pebble-timer's is 260 bytes in) where FreeType accepts it. Tested in Chromium against a deployed bundle: repo URL to a running app in 13 seconds, PebbleKit JS included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
Summary
Adds
tools/qemu-wasm/: a browser-based PebbleOS emulator built on the coredevices/qemu generic Pebble machines compiled to WebAssembly, plus the full toolchain and web shell around it. Live at https://ericmigi.github.io/pebble-qemu-wasm/. Everything is contained undertools/qemu-wasm/— no firmware or build-system changes.What's in here, layer by layer:
web/): bootspebble-emerywith release firmware, canvas display via shared-memory framebuffer exports, buttons via an atomics-polled bitmask, serial console.web/pebble-transport.js,app-install.js,pbw.js,store.js): a JS implementation of the phone side of the Pebble Protocol — QemuProtocol framing over a shared-memory UART bridge, phone-version handshake, and the full 4.x app-install flow (BlobDB → AppFetch → PutBytes with the legacy defective CRC, verified against the firmware's test vectors). Users install apps by pasting an apps.repebble.com link, uploading, or drag-dropping a.pbw.web/pkjs-runtime.js,appmessage.js): apps withpebble-js-app.jsget their JS run in a sandboxed iframe with aPebbleshim, wired to the watch over AppMessage (endpoint 0x30), lifecycle driven by app_run_state notifications. Includes config-page support via thereturn_toconvention and an optional CORS-proxy fallback (pkjs-proxy/, a Cloudflare Worker).patches/,jit/): TCI-interpreter patches (RAM-backed framebuffer, inline TLB fast path, ASYNCIFY_REMOVE lists) take scroll fps from 2.1 → 4.5; the TCG→wasm JIT overlay (ktock/qemu-wasm tree, QEMU 10.2 port of the pebble device set) reaches ~17 fps and 15 s boots. The JIT overlay documents the 10.1→10.2 API drift and should be retired when the wasm TCG backend lands upstream.site-dist/): prebuilt gzipped build +apply.shfor the GitHub Pages site. ~3.9 MB of binaries — flagging for maintainer judgment; happy to move these to release assets or CI artifacts in a follow-up if binaries in-tree are unwanted.Verification
.pbw(BlobDB ack → AppFetch round trip → CRC-clean PutBytes → app fetch result 0), C↔JS AppMessage round trip both directions.🤖 Generated with Claude Code
https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Generated by Claude Code